package com.heima.article.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApArticleConfigMapper;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.article.service.ApArticleService;
import com.heima.article.service.ArticleFreemarkerService;
import com.heima.common.constants.ArticleConstants;
import com.heima.common.constants.BehaviorConstants;
import com.heima.common.redis.CacheService;
import com.heima.model.article.dtos.ArticleCommentDto;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.dtos.ArticleHomeDto;

import com.heima.model.article.dtos.ArticleInfoDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleConfig;
import com.heima.model.article.pojos.ApArticleContent;
import com.heima.model.article.vos.ArticleCommnetVo;
import com.heima.model.article.vos.HotArticleVo;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.mess.ArticleVisitStreamMess;
import com.heima.model.user.pojos.ApUser;
import com.heima.model.wemedia.dtos.StatisticsDto;
import com.heima.utils.common.DateUtils;
import com.heima.utils.thread.AppThreadLocalUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;


@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl
        extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {

    // 单页最大加载的数字
    private final static short MAX_PAGE_SIZE = 50;

    @Autowired
    private ApArticleMapper apArticleMapper;

    /**
     * 根据参数加载文章列表
     * @param dto
     * @param loadtype 1为加载更多  2为加载最新
     * @return
     */
    @Override
    public ResponseResult load(ArticleHomeDto dto, Short loadtype) {
        //1.校验参数
        Integer size = dto.getSize();
        if(size == null || size == 0){
            size = 10;//默认加载10条
        }
        //分页的值不超过50
        size = Math.min(size,MAX_PAGE_SIZE);
        dto.setSize(size);

        //类型参数检验，type可能为空？？
        if(!loadtype.equals(ArticleConstants.LOADTYPE_LOAD_MORE)
                &&!loadtype.equals(ArticleConstants.LOADTYPE_LOAD_NEW)){
            loadtype = ArticleConstants.LOADTYPE_LOAD_MORE;//默认是加载更多
        }
        //文章频道校验
        if(StringUtils.isEmpty(dto.getTag())){
            //给个默认频道
            dto.setTag(ArticleConstants.DEFAULT_TAG);
        }

        //时间校验
        if(dto.getMaxBehotTime() == null) {
            dto.setMaxBehotTime(new Date());
        }
        if(dto.getMinBehotTime() == null) {
            dto.setMinBehotTime(new Date());
        }
        //2.查询数据
        List<ApArticle> apArticles = apArticleMapper.loadArticleList(dto, loadtype);

        //3.结果封装
        ResponseResult responseResult = ResponseResult.okResult(apArticles);
        return responseResult;
    }


    /**
     * 加载文章列表
     * @param dto
     * @param type      1 加载更多   2 加载最新
     * @param firstPage true  是首页  flase 非首页
     * @return
     */
    @Override
    public ResponseResult load2(ArticleHomeDto dto, Short type, boolean firstPage) {
        if(firstPage){
            //是首页，从缓存中获取数据                                                       某一个频道
            String jsonStr = cacheService.get(ArticleConstants.HOT_ARTICLE_FIRST_PAGE + dto.getTag());
            if(StringUtils.isNotBlank(jsonStr)){
                List<HotArticleVo> hotArticleVoList = JSON.parseArray(jsonStr, HotArticleVo.class);
                ResponseResult responseResult = ResponseResult.okResult(hotArticleVoList);
                return responseResult;
            }
        }
        //不是首页，走正常查询逻辑
        return load(dto,type);
    }

    @Autowired
    private ApArticleConfigMapper apArticleConfigMapper;

    @Autowired
    private ApArticleContentMapper apArticleContentMapper;

    @Autowired
    private ArticleFreemarkerService articleFreemarkerService;

    /**
     * 保存app端相关文章 涉及到三张表 ap_article ap_article_config ap_article_content
     * @param dto
     * @return
     */
    @Override
    public ResponseResult saveArticle(ArticleDto dto) {

//在leadnews-wemedia模块中配置了：
//      feign:
//  # 开启feign对hystrix熔断降级的支持
//         hystrix:
//            enabled: true
//  # 修改调用超时时间
//         client:
//            config:
//            default:
//              connectTimeout: 2000
//              readTimeout: 2000
//测试肯定超时，就会走当前的远程调用服务降级逻辑leadnews-feign-api的IArticleClientFallback.java
//会报错："WmNewsAutoScanServiceImpl-保存app端相关文章数据失败"
//        try {
//            Thread.sleep(3000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }

        //1.检查参数
        if (dto == null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        ApArticle apArticle = new ApArticle();
        BeanUtils.copyProperties(dto,apArticle);

        //2.判断是否存在
        if(dto.getId() == null){
            //2.1不存在id 保存 文章 文章配置 文章内容
            System.out.println("文章id本没有值？："+apArticle.getId());
            save(apArticle);//id 由数据库自动生成(雪花自增？？)
            System.out.println("文章id回显？："+apArticle.getId());

            //保存文章配置
            //默认开启评论，默认。。写死在配置表实体类中的(有参构造函数中)   创建带有这些默认值的对象
            ApArticleConfig apArticleConfig = new ApArticleConfig(apArticle.getId());
            //apArticleConfig.setArticleId(apArticle.getId());//保存之后的文章id
            apArticleConfigMapper.insert(apArticleConfig);

            //保存文章内容(用于？？？？)
            ApArticleContent apArticleContent = new ApArticleContent();
            apArticleContent.setArticleId(apArticle.getId());
            apArticleContent.setContent(dto.getContent());//内容从文章dto中获取
            apArticleContentMapper.insert(apArticleContent);

        }else{
            //2.2存在id 修改 文章 文章内容 文章配置(不需要改，用默认值就行)
            //修改文章
            updateById(apArticle);
            //修改文章内容
            //先查到对应的文章内容
            ApArticleContent apArticleContent = apArticleContentMapper.selectOne
                    (Wrappers.<ApArticleContent>lambdaQuery()
                            .eq(ApArticleContent::getArticleId, apArticle.getId()));
            apArticleContent.setContent(dto.getContent());
            apArticleContentMapper.updateById(apArticleContent);
        }

        //文章端创建app相关文章时，生成文章详情静态页上传到minio中
        //异步调用，生成文章详情静态页，并上传到minio中
        //(新增文章(WmNewsAutoScanServiceImpl里的autoScanWmNews就已经审核完成了)，
        // 发送消息通知搜索微服务端(消息消费者)，创建索引，添加数据到索引库，不能在这里实现，
        // 要在文章详情页生成静态页之后再发送消息(需要设置SearchArticleVo的静态staticUrl属性(es数据库中的字段))，
        // 因为进入buildArticleToMinio后才会生成SearchArticleVo中的静态url private String staticUrl;)
        //所以发送消息通知搜索微服务端(消息消费者)，创建索引，添加数据到索引库，
        // 需要在buildArticleToMinio()里实现
        articleFreemarkerService.buildArticleToMinio(apArticle, dto.getContent());

        //3.返回结果 文章的id
        System.out.println("文章的id是："+apArticle.getId());
        ResponseResult responseResult = ResponseResult.okResult(apArticle.getId());
        System.out.println("saveArticle的responseResult.getCode()是："+responseResult.getCode());
        return ResponseResult.okResult(apArticle.getId());
    }


    @Autowired
    private CacheService cacheService;

    /**
     * 加载文章详情 数据回显
     * @param dto
     * @return
     */
    @Override
    public ResponseResult loadArticleBehavior(ArticleInfoDto dto) {

        //0.检查参数
        if (dto == null || dto.getArticleId() == null || dto.getAuthorId() == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        //{ "isfollow": true, "islike": true,"isunlike": false,"iscollection": true }
        boolean isfollow = false, islike = false, isunlike = false, iscollection = false;

        ApUser user = AppThreadLocalUtil.getUser();
        if(user != null){
            //喜欢行为
            String likeBehaviorJson = (String) cacheService.hGet(BehaviorConstants.LIKE_BEHAVIOR + dto.getArticleId().toString(), user.getId().toString());
            if(StringUtils.isNotBlank(likeBehaviorJson)){
                islike = true;
            }
            //不喜欢的行为
            String unLikeBehaviorJson = (String) cacheService.hGet(BehaviorConstants.UN_LIKE_BEHAVIOR + dto.getArticleId().toString(), user.getId().toString());
            if(StringUtils.isNotBlank(unLikeBehaviorJson)){
                isunlike = true;
            }
            //是否收藏
            String collctionJson = (String) cacheService.hGet(BehaviorConstants.COLLECTION_BEHAVIOR+user.getId(),dto.getArticleId().toString());
            if(StringUtils.isNotBlank(collctionJson)){
                iscollection = true;
            }

            //是否关注
            Double score = cacheService.zScore(BehaviorConstants.APUSER_FOLLOW_RELATION + user.getId(), dto.getAuthorId().toString());
            System.out.println(score);
            if(score != null){
                isfollow = true;
            }

        }

        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("isfollow", isfollow);
        resultMap.put("islike", islike);
        resultMap.put("isunlike", isunlike);
        resultMap.put("iscollection", iscollection);

        return ResponseResult.okResult(resultMap);
    }

    /**
     * 更新文章的分值  同时更新缓存中的热点文章数据
     * @param mess ArticleIncrHandleListener里@KafkaListener(topics = HotArticleConstants.HOT_ARTICLE_INCR_HANDLE_TOPIC)
     *             监听到的数值
     */
    @Override
    public void updateScore(ArticleVisitStreamMess mess) {
        //1.更新文章的阅读、点赞、收藏、评论的数量
        ApArticle apArticle = updateArticle(mess);
        //2.重新计算文章的分值，HotArticleServiceImpl里的computeScore方法是private类型，无法注入调用，
        // 需要改成public，并且还要新建HotArticleService接口
        Integer score = computeScore(apArticle);
        //TODO 当天的分值要重新计算，x3(今日的数据最重要)？？？？ ，因为文章的分值是实时更新的，不能用缓存中的数据
        score = score * 3;
        log.info("更新当前文章的分值x3，文章：{}，的分值：{}", apArticle.getTitle(), score);

        //3.替换(更新)当前文章对应频道的热点数据
        log.info("替换(更新)当前文章对应频道的热点数据");
        replaceDataToRedis(apArticle, score, ArticleConstants.HOT_ARTICLE_FIRST_PAGE + apArticle.getChannelId());

        //4.替换(更新)推荐(默认频道)对应的热点数据
        log.info("替换(更新)推荐(默认频道)对应的热点数据");
        replaceDataToRedis(apArticle, score, ArticleConstants.HOT_ARTICLE_FIRST_PAGE + ArticleConstants.DEFAULT_TAG);

    }

    /**
     * 替换数据并且存入到redis
     * @param apArticle
     * @param score
     * @param s
     */
    private void replaceDataToRedis(ApArticle apArticle, Integer score, String s) {
        log.info("替换数据并且存入到redis，文章：{}，分值：{}，key：{}", apArticle.getTitle(), score, s);
        String articleListStr = cacheService.get(s);
        log.info("替换数据并且存入到redis，文章：{}，分值：{}，key：{}，articleListStr：{}", apArticle.getTitle(), score, s, articleListStr);
        if (StringUtils.isNotBlank(articleListStr)) {
            List<HotArticleVo> hotArticleVoList = JSON.parseArray(articleListStr, HotArticleVo.class);

            boolean flag = true;

            for (HotArticleVo hotArticleVo : hotArticleVoList) {
                //如果缓存中存在该文章，只更新分值
                if (hotArticleVo.getId().equals(apArticle.getId())) {
                    hotArticleVo.setScore(score);
                    log.info("缓存中已存在该文章：{}，只更新分值分值：{}", hotArticleVo.getTitle(), hotArticleVo.getScore());
                    flag = false;
                    break;//跳出循环
                }
            }

            //如果缓存中不存在，查询缓存中分值最小的一条数据，进行分值的比较，如果当前文章的分值大于缓存中的数据，就替换
            //如果上面已经找到缓存了，flag = false 就不执行下面的代码
            if (flag) {
                if (hotArticleVoList.size() >= 30) {
                    //缓存中数据已经满30条了，就直接替换掉分值最小的一条数据
                    //1.先排序
                    log.info("排序之前的数据：{}", hotArticleVoList);
                    hotArticleVoList = hotArticleVoList
                            //通过调用 stream() 方法，我们可以对集合中的元素进行一系列的操作，如过滤、排序、映射等。
                            .stream()
                            //创建一个比较器，该比较器基于 HotArticleVo 对象的 getScore 方法返回的值来进行比较。
                            // 这里使用了方法引用，表示对每个 HotArticleVo 对象调用 getScore() 方法。
                            //将上述比较器的顺序反转。即从高到低排序（通常情况下，sorted() 默认是从小到大排序，使用 reversed() 可以实现降序排序）。
                            .sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
                            //将排序后的元素重新收集到一个列表中。collect() 是 Terminal 操作之一，表示流处理的结束，并将结果以指定的方式汇集到一个容器中。
                            // 这里我们使用 Collectors.toList() 将结果收集回 List<HotArticleVo> 类型。
                            .collect(Collectors.toList());
                    log.info("重新排序后的数据：{}", hotArticleVoList);
                    //2.取最后(小)的数据(如果一个列表的大小（size）是 n，那么它的索引范围是从 0 到 n-1。)
                    HotArticleVo lastHot = hotArticleVoList.get(hotArticleVoList.size() - 1);
                    //如果当前文章的分值大于缓存中的数据，就替换，否则不用管
                    if (lastHot.getScore() < score) {
                        hotArticleVoList.remove(lastHot);
                        HotArticleVo hot = new HotArticleVo();
                        BeanUtils.copyProperties(apArticle, hot);
                        hot.setScore(score);
                        hotArticleVoList.add(hot);
                        log.info("缓存中数据已经满30条，替换当前文章对应频道的热点数据：{}",hot);
                    }
                } else {
                    //缓存中数据没有满30条，就直接新增即可
                    HotArticleVo hot = new HotArticleVo();
                    BeanUtils.copyProperties(apArticle, hot);
                    hot.setScore(score);
                    hotArticleVoList.add(hot);
                    log.info("缓存中数据没有满30条，就直接新增",hot);
                }
            }
            //缓存到redis
            //存入的数据不能保证有序(因为新增的文章的分值只是把最小的那个替换掉了，它不一定就是比前面的都小)，所以需要再次排序
            hotArticleVoList = hotArticleVoList
                    .stream()
                    .sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
                    .collect(Collectors.toList());
            cacheService.set(s, JSON.toJSONString(hotArticleVoList));
            log.info("缓存到了redis中");
        }
    }

    /**
     * 更新文章行为数量
     * @param mess
     */
    private ApArticle updateArticle(ArticleVisitStreamMess mess) {
        ApArticle apArticle = getById(mess.getArticleId());
        //拿到原有的数量 + mess中新增的数量
        log.info("更新文章行为数量，新增的点赞数：{},新增的收藏数：{},新增的评论数：{},新增的浏览数：{}",
                mess.getLike(),mess.getCollect(),mess.getComment(),mess.getView());
        apArticle.setCollection(apArticle.getCollection()==null?0:apArticle.getCollection()+mess.getCollect());
        apArticle.setComment(apArticle.getComment()==null?0:apArticle.getComment()+mess.getComment());
        apArticle.setLikes(apArticle.getLikes()==null?0:apArticle.getLikes()+mess.getLike());
        apArticle.setViews(apArticle.getViews()==null?0:apArticle.getViews()+mess.getView());
        updateById(apArticle);
        log.info("重新计算文章的行为数量: {},{},{},{}",
                apArticle.getLikes(), apArticle.getCollection(), apArticle.getComment(),apArticle.getViews());
        return apArticle;
    }

    /**
     * 重新计算文章的具体分值(重新计算)
     * @param apArticle
     * @return
     */
    //和HotArticleServiceImpl里的computeScore(初次计算)一样
    private Integer computeScore(ApArticle apArticle) {
        Integer score = 0;
        if(apArticle.getLikes() != null){
            score += apArticle.getLikes() * ArticleConstants.HOT_ARTICLE_LIKE_WEIGHT;
        }
        if(apArticle.getViews() != null){
            score += apArticle.getViews();
        }
        if(apArticle.getComment() != null){
            score += apArticle.getComment() * ArticleConstants.HOT_ARTICLE_COMMENT_WEIGHT;
        }
        if(apArticle.getCollection() != null){
            score += apArticle.getCollection() * ArticleConstants.HOT_ARTICLE_COLLECTION_WEIGHT;
        }
        log.info("重新计算文章的具体分值: {}", score);
        return score;
    }

    /**
     * 图文统计查询时间范围内文章的点赞和收藏数据
     * @param wmUserId
     * @param beginDate
     * @param endDate
     * @return
     */
    @Override
    public ResponseResult queryLikesAndConllections(Integer wmUserId, Date beginDate, Date endDate) {
        Map map = apArticleMapper.queryLikesAndConllections(wmUserId, beginDate, endDate);
        return ResponseResult.okResult(map);
    }

    /**
     * 分页查询 图文统计
     * (分页展示文章列表，展示当前时间范围内的具体文章阅读、评论、收藏的数量。)
     * @param dto
     * @return
     */
    @Override
    public PageResponseResult newPage(StatisticsDto dto) {

        //类型转换(将 DTO 对象中的开始日期和结束日期（字符串格式）转换为 Date 类型。
        // DateUtils.stringToDate() 是一个工具方法，用于将字符串转换为 Date 对象。)
        Date beginDate = DateUtils.stringToDate(dto.getBeginDate());
        Date endDate = DateUtils.stringToDate(dto.getEndDate());
        //检查参数StatisticsDto继承了PageRequestDto，
        // PageRequestDto里有checkParam()方法，用于检查参数是否合法。)
        dto.checkParam();
        //分页查询
        IPage page = new Page(dto.getPage(), dto.getSize());
        //构建查询条件
        LambdaQueryWrapper<ApArticle> lambdaQueryWrapper =
                Wrappers.<ApArticle>lambdaQuery()
                //查询的是自己的文章
                .eq(ApArticle::getAuthorId, dto.getWmUserId())
                //查询条件为文章的发布时间在 beginDate 和 endDate 之间。
                .between(ApArticle::getPublishTime,beginDate, endDate)
                //指定查询的字段，这里选择了文章的 ID、标题、点赞数、收藏数、评论数和浏览数。
               .select(ApArticle::getId,ApArticle::getTitle,
                       ApArticle::getLikes,ApArticle::getCollection,
                       ApArticle::getComment,ApArticle::getViews);
        //按发布时间降序排列。
        lambdaQueryWrapper.orderByDesc(ApArticle::getPublishTime);
        //使用构建好的分页对象和查询条件执行查询，并将结果赋值回 page 对象。
        // 这通常会返回符合条件的文章记录。
        page = page(page,lambdaQueryWrapper);
        //封装分页信息，包括当前页码、每页大小和总记录数（通过 page.getTotal() 获取）。
        PageResponseResult responseResult =
                new PageResponseResult(dto.getPage(),dto.getSize(),(int)page.getTotal());
        //将查询结果（文章记录）设置到响应结果中。
        responseResult.setData(page.getRecords());

        return responseResult;
    }

    /**
     * 查询文章评论的状态(评论管理分页查询，展示文章标题，评论状态，评论总量。。)
     * @param dto
     * @return
     */
    @Override
    public PageResponseResult findNewsComments(ArticleCommentDto dto) {

        //从 dto 对象中获取当前页码
        Integer currentPage = dto.getPage();
        //计算数据库查询所需的起始位置。这里将当前页码减去 1（因为页码通常从 1 开始，而数据库查询从 0 开始），
        // 然后乘以每页的大小（dto.getSize()），将结果设置回 dto 对象的页码。
        dto.setPage((dto.getPage()-1)*dto.getSize());
        //获取当前页的评论列表，结果存储在 list 变量中。
        log.info("查询文章评论的状态参数: {}",dto.toString());
        List<ArticleCommnetVo> list = apArticleMapper.findNewsComments(dto);
        //获取符合条件的评论总数
        log.info("查询文章评论的状态，当前页评论列表: {}",list);
        int count = apArticleMapper.findNewsCommentsCount(dto);
        log.info("查询文章评论的状态，当前页码: {},每页大小: {},文章数量: {}",
                currentPage,dto.getSize(),count);
        //查询结果的总记录数count。这是分页功能中一个非常重要的部分，
        // 因为它允许前端或调用者了解在当前查询条件下，数据库中总共有多少条记录。
        // 通过这个信息，前端可以计算出总页数，从而决定如何展示分页控件。
        //构建响应结果(参数包括当前页码 currentPage、每页的大小 dto.getSize() 和文章总数 count)
        PageResponseResult responseResult
                = new PageResponseResult(currentPage,dto.getSize(),count);
        responseResult.setData(list);
        return responseResult;
    }


}